<?php
/*********************************************
 *  CPG Dragonfly™ CMS
 *********************************************
	Copyright © since 2010 by CPG-Nuke Dev Team
	http://dragonflycms.org

	Dragonfly is released under the terms and conditions
	of the GNU GPL version 2 or any later version

	NOTE: we work with 14 decimals, because if bcmath isn't loaded the code
	converts the decimal to (float).
	The size of a float is platform-dependent, although a maximum of ~1.8e308
	with a double-precision of roughly 14 decimal digits is a common value.
*/

namespace Poodle\Math;

abstract class Base
{
	public static function longToBinary($long)
	{
		$cmp = static::cmp($long, 0);
		if ($cmp < 0) {
			trigger_error('Poodle\\Math::longToBinary takes only positive integers.', E_USER_ERROR);
			return null;
		}
		if (0 == $cmp) { return "\x00"; }

		$bytes = array();
		while (static::cmp($long, 0) > 0) {
			array_unshift($bytes, static::mod($long, 256));
			$long = static::div($long, pow(2, 8));
		}
		if ($bytes && ($bytes[0] > 127)) { array_unshift($bytes, 0); }

		$string = '';
		foreach ($bytes as $byte) { $string .= pack('C', $byte); }
		return $string;
	}

	public static function binaryToLong($str)
	{
		if (null === $str) { return null; }
		# Use array_merge to return a zero-indexed array instead of a one-indexed array.
		$bytes = array_merge(unpack('C*', $str));
		if ($bytes && ($bytes[0] > 127)) {
			trigger_error('Poodle\\Math::bytesToNum works only for positive integers.', E_USER_WARNING);
			return null;
		}
		$n = static::init(0);
		foreach ($bytes as $byte) { $n = static::add(static::mul($n, pow(2, 8)), $byte); }
		return $n;
	}

	public static function base64ToLong($str)
	{
		$b64 = base64_decode($str);
		return (false === $b64) ? false : self::binaryToLong($b64);
	}

	public static function longToBase64($str)
	{
		return base64_encode(self::longToBinary($str));
	}

	public static function rand($stop)
	{
		static $duplicate_cache = array();

		// Used as the key for the duplicate cache
		$rbytes = self::longToBinary($stop);

		if (array_key_exists($rbytes, $duplicate_cache)) {
			list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
		} else {
			if ($rbytes[0] == "\x00") {
				$nbytes = mb_strlen($rbytes,'8bit') - 1;
			} else {
				$nbytes = mb_strlen($rbytes,'8bit');
			}
			$mxrand = static::pow(256, $nbytes);
			# If we get a number less than this, then it is in the duplicated range.
			$duplicate = static::mod($mxrand, $stop);
			if (count($duplicate_cache) > 10) {
				$duplicate_cache = array();
			}
			$duplicate_cache[$rbytes] = array($duplicate, $nbytes);
		}

		do {
			$bytes = "\x00" . \Poodle\Random::bytes($nbytes);
			$n = self::binaryToLong($bytes);
			// Keep looping if this value is in the low duplicated range
		} while (static::cmp($n, $duplicate) < 0);

		return static::mod($n, $stop);
	}

	public static function getValid($n)
	{
		if (is_float($n)) { $n = number_format($n,14,'.',''); }
		if (preg_match('/^([-+]?\\d+)(\\.\\d+)?$/',$n,$m)) { return $m[1].(isset($m[2])?rtrim($m[2], '0.'):''); }
		return false;
	}

	public static function getScale($l, $r)
	{
		if (!preg_match('/^[-+]?\d+(?:\.(\d+))?$/',$l,$ld)
		 || !preg_match('/^[-+]?\d+(?:\.(\d+))?$/',$l,$rd)) return '0';
		// remove ending zeroes
		$ld = isset($ld[1]) ? rtrim($ld[1],'0') : '';
		$rd = isset($rd[1]) ? rtrim($rd[1],'0') : '';
		return max(strlen($ld), strlen($rd));
	}
}

abstract class BcMath extends Base
{
	const ENGINE = 'bcmath';

	public static function add($l, $r) { return self::doMath('add', $l, $r); }
	public static function cmp($l, $r) { return bccomp(self::getValid($l), self::getValid($r), 14); }
	public static function div($l, $r) { return self::doMath('div', $l, $r); }
	public static function mod($l, $m) { return bcmod(self::getValid($l), self::getValid($m)); }
	public static function mul($l, $r) { return self::doMath('mul', $l, $r); }
	public static function pow($l, $r) { return self::doMath('pow', $l, $r); }
	public static function powmod($l, $r, $m) { return self::doMath('powmod', $l, $r, $m); }
	public static function sqrt($o)    { return self::doMath('sqrt', $o); }
	public static function sub($l, $r) { return self::doMath('sub', $l, $r); }

	public static function init($number, $base = 10) { return $number; }

	protected static function doMath()
	{
		$args = func_get_args();
		$fn = 'bc'.array_shift($args);
		foreach ($args as $i => $v) { $args[$i] = self::getValid($v); }
		$args[] = 14; // add scale
		return self::getValid(call_user_func_array($fn, $args));
	}
}

/** GMP only works with integers, not with decimals
abstract class GMP extends Base
{
	const ENGINE = 'gmp';

	public static function add($l, $r) { return self::doMath('add', $l, $r); }
	public static function cmp($l, $r) { return gmp_cmp(self::getValid($l), self::getValid($r)); }
	public static function div($l, $r) { return self::doMath('div_q', $l, $r); }
	public static function mod($l, $m) { return self::doMath('mod', $l, $m); }
	public static function mul($l, $r) { return self::doMath('mul', $l, $r); }
	public static function pow($l, $r) { return self::doMath('pow', $l, $r); }
	public static function powmod($l, $r, $m) { return self::doMath('powm', $l, $r, $m); }
	public static function sub($l, $r) { return self::doMath('sub', $l, $r); }
	public static function sqrt($o)    { return self::doMath('sqrt', $o); }

	public static function init($number, $base = 10) { return gmp_init($number, $base); }

	protected static function doMath()
	{
		$args = func_get_args();
		$fn = 'gmp_'.array_shift($args);
		foreach ($args as $i => $v) { $args[$i] = self::getValid($v); }
		print_r($args);
		return gmp_strval(call_user_func_array($fn, $args));
	}
}
*/

abstract class None extends Base
{
	const ENGINE = null;

	protected static function shiftDecimal($v)
	{
		$v = '0'.self::getValid($v);
		$p = strpos($v, '.') ?: strlen($v);
		$v = substr($v,0,$p-2) . '.' . str_replace('.','',substr($v,$p-2,14));
		return self::getValid(ltrim($v,'0'));
	}

	public static function add($l, $r)
	{
		return self::shiftDecimal(self::getValid($l) * 100 + self::getValid($r) * 100);
	}

	public function cmp($l, $r)
	{
		if (!preg_match('/^([-+]?\d+)(?:\.(\d+))?$/',$l,$l)
		 || !preg_match('/^([-+]?\d+)(?:\.(\d+))?$/',$r,$r)) return false;
		$ln = ('-' == $l[1][0]) ? '-' : '';
		$rn = ('-' == $r[1][0]) ? '-' : '';
		$l[1] = (int)$l[1];
		$r[1] = (int)$r[1];
		if ($l[1] == $r[1]) {
			$l[2] = isset($l[2]) ? (int)($ln.substr($l[2],0,9)) : 0;
			$r[2] = isset($r[2]) ? (int)($rn.substr($r[2],0,9)) : 0;
			if ($l[2] == $r[2]) return 0;
			return $l[2]>$r[2] ? 1 : -1;
		}
		return $l[1]>$r[1] ? 1 : -1;
	}

	public function div($l, $r)
	{
		return self::shiftDecimal(self::getValid($l) * 100 / self::getValid($r));
	}

	public static function mod($l, $m)
	{
		return getValid($l) % self::getValid($r);
	}

	public function mul($l, $r)
	{
		return self::shiftDecimal(self::getValid($l) * 100 * self::getValid($r));
	}

	public function sub($l, $r)
	{
		return self::shiftDecimal(self::getValid($l) * 100 + self::getValid($r) * 100);
	}

	public static function pow($l, $r)
	{
		return pow($l, $r);
		$a = 1;
		while ($r) {
			$a = self::mul($r, $l);
			$r = self::sub($r, "1");
		}
		return self::getValid($a);
	}

	public static function powmod($l, $r, $m) { return self::mod(self::pow($l, $r), $m); }

	public static function sqrt($o) { return self::getValid(sqrt($o)); }

	public static function init($number, $base = 10) { return $number; }
}

/*if (extension_loaded('gmp') && !defined('GMP_BUGGY'))
{
	class_alias('Poodle\\Math\\GMP', 'Poodle\\Math');
}
else*/
if (extension_loaded('bcmath'))
{
	class_alias('Poodle\\Math\\BcMath', 'Poodle\\Math');
}
else
{
	class_alias('Poodle\\Math\\None', 'Poodle\\Math');
}
